package com.project.website.canvas.client.canvastools.textedit; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import com.google.common.base.Objects; import com.google.common.collect.Lists; import com.google.gwt.core.client.GWT; import com.google.gwt.core.client.Scheduler.ScheduledCommand; import com.google.gwt.dom.client.Element; import com.google.gwt.dom.client.Node; import com.google.gwt.dom.client.OptionElement; import com.google.gwt.event.dom.client.BlurEvent; import com.google.gwt.event.dom.client.BlurHandler; import com.google.gwt.event.dom.client.ChangeEvent; import com.google.gwt.event.dom.client.ChangeHandler; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.event.dom.client.DomEvent; import com.google.gwt.event.dom.client.KeyDownEvent; import com.google.gwt.event.dom.client.MouseDownEvent; import com.google.gwt.event.dom.client.MouseUpEvent; import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.uibinder.client.UiBinder; import com.google.gwt.uibinder.client.UiField; import com.google.gwt.user.client.Event; import com.google.gwt.user.client.Event.NativePreviewEvent; import com.google.gwt.user.client.Event.NativePreviewHandler; import com.google.gwt.user.client.ui.Button; import com.google.gwt.user.client.ui.Composite; import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.HTMLPanel; import com.google.gwt.user.client.ui.InlineLabel; import com.google.gwt.user.client.ui.ListBox; import com.google.gwt.user.client.ui.Widget; import com.project.shared.client.events.SimpleEvent; import com.project.shared.client.handlers.RegistrationsManager; import com.project.shared.client.html5.Range; import com.project.shared.client.html5.impl.RangeUtils; import com.project.shared.client.html5.impl.SelectionImpl; import com.project.shared.client.utils.DocumentUtils; import com.project.shared.client.utils.ElementUtils; import com.project.shared.client.utils.EventUtils; import com.project.shared.client.utils.SchedulerUtils; import com.project.shared.client.utils.StyleUtils; import com.project.shared.client.utils.widgets.ListBoxUtils; import com.project.shared.data.funcs.Func; import com.project.shared.data.funcs.Func.Action; import com.project.website.canvas.client.resources.CanvasResources; import com.project.website.canvas.client.shared.widgets.ColorPicker; public class TextEditToolbarImpl extends Composite implements TextEditToolbar { private static TextEditToolbarImplUiBinder uiBinder = GWT.create(TextEditToolbarImplUiBinder.class); interface TextEditToolbarImplUiBinder extends UiBinder<Widget, TextEditToolbarImpl> {} private final RegistrationsManager registrationsManager = new RegistrationsManager(); @UiField HTMLPanel rootPanel; private Widget _editedWidget; private ArrayList<ToolbarButtonInfo> buttonInfos = new ArrayList<ToolbarButtonInfo>(); private ArrayList<Func<Void,Void>> onUnloadFuncs = new ArrayList<Func<Void,Void>>(); private HashSet<Range> savedRanges = new HashSet<Range>(); private final SimpleEvent<Void> _buttonAppliedEvent = new SimpleEvent<Void>(); public TextEditToolbarImpl() { initWidget(uiBinder.createAndBindUi(this)); initButtons(); } @Override public Widget getEditedWidget() { return this._editedWidget; } @Override public void setEditedWidget(Widget elem) { if (elem == this._editedWidget) { this.updateButtonStates(); return; } this._editedWidget = elem; this.clearRegistrations(); if (null != elem) { this.setRegistrations(); } } @Override protected void onLoad() { super.onLoad(); if (null != this._editedWidget) { this.setRegistrations(); } } @Override protected void onUnload() { this.clearRegistrations(); for (Func<Void,Void> func : this.onUnloadFuncs) { func.apply(null); } super.onUnload(); } public HandlerRegistration addButtonAppliedHandler(SimpleEvent.Handler<Void> handler) { return this._buttonAppliedEvent.addHandler(handler); } private void clearRegistrations() { this.registrationsManager.clear(); } private void setRegistrations() { if (this.registrationsManager.hasRegistrations()) { // already set. return; } final TextEditToolbarImpl that = this; if (null != this._editedWidget) { this.registrationsManager.add(this._editedWidget.addDomHandler(new BlurHandler(){ @Override public void onBlur(BlurEvent event) { that.saveSelectedRanges(); }}, BlurEvent.getType())); } this.registrationsManager.add(Event.addNativePreviewHandler(new NativePreviewHandler() { ScheduledCommand updateButtonStatesCommand = new ScheduledCommand() { @Override public void execute() { if (that.isActiveElementTree()) { that.updateButtonStates(); } that._buttonAppliedEvent.dispatch(null); } }; @Override public void onPreviewNativeEvent(NativePreviewEvent event) { if (EventUtils.nativePreviewEventTypeIsAny(event, new DomEvent.Type<?>[] { MouseDownEvent.getType(), MouseUpEvent.getType(), KeyDownEvent.getType() })) { that.saveSelectedRanges(); SchedulerUtils.OneTimeScheduler.get().scheduleDeferredOnce(updateButtonStatesCommand); } } })); this.updateButtonStates(); } protected boolean isActiveElementTree() { if (null == this._editedWidget) { return false; } return DocumentUtils.isActiveElementTree(this._editedWidget.getElement()); } private void initButtons() { // setSimpleCssValueButton("fontWeight", "bold", "Bold"); this.addCssStringValueButton("fontWeight", new String[] { "400", "normal" }, new String[] { "700", "bold" }, "Bold", false); this.addCssStringValueButton("fontStyle", "normal", "italic", "Italic", false); this.addCssStringValueButton("textDecoration", "none", "underline", "Underline", false); this.addCssStringValueListBox("fontFamily", "Font:", true, getFontFamilies(), CanvasResources.INSTANCE.main().textEditToolbarFontFamilyList()); this.addCssStringValueListBox("fontSize", "Size:", false, getFontSizes()); // TODO replace these two with color-pickers: this.addColorPicker("color", "Color:"); this.addCssStringValueButton("direction", "ltr", "rtl", "Direction", true); } private void addColorPicker(String cssProperty, String title) { final TextEditToolbarImpl that = this; final ColorPicker colorPicker = new ColorPicker(); this.addTitledToolbarItem(title, colorPicker); this.onUnloadFuncs.add(new Func.VoidAction() { @Override public void exec() { colorPicker.setPickerVisible(false); } }); final ToolbarButtonInfo buttonInfo = new ToolbarButtonInfo() { @Override public void updateButtonStatus(Element testedElement) { String cssColorString = StyleUtils.getComputedStyle(testedElement, null).getColor(); colorPicker.getElement().getStyle().setBackgroundColor(cssColorString); // Just to make the text invisible: colorPicker.getElement().getStyle().setColor(cssColorString); } @Override public void unset(Element elem) { // Do nothing. TODO: Maybe add a default value for un-setting. } @Override public void set(Element elem) { String cssColorString = StyleUtils.getComputedStyle(colorPicker.getElement(), null).getBackgroundColor(); elem.getStyle().setColor(cssColorString); } @Override public boolean isSet(Element elem) { return false; } @Override public boolean isOnRootElemOnly() { return false; } }; // colorPicker.addChangeHandler(new ChangeHandler() { // @Override public void onChange(ChangeEvent event) // { // that.buttonPressed(buttonInfo); // } // }); colorPicker.addDomHandler(new ChangeHandler(){ @Override public void onChange(ChangeEvent event) { that.buttonPressed(buttonInfo); }}, ChangeEvent.getType()); this.addButtonInfo(buttonInfo); } private Iterable<String> getFontSizes() { int[] sizes = new int[] { 8, 10, 12, 13, 14, 16, 18, 20, 24, 28, 32, 36, 48, 72, 96, 144 }; ArrayList<String> values = new ArrayList<String>(); for (int size : sizes) { values.add(String.valueOf(size) + "px"); } return values; } private Iterable<String> getFontFamilies() { return Lists.newArrayList("Arial", "Georgia", "Julee", "Monospace", "Verdana", "Times", "Tulpen One"); } private void addCssStringValueListBox(final String cssProperty, String title, final boolean setOptionsStyles, final Iterable<String> values, String... addStyleNames) { final TextEditToolbarImpl that = this; final ListBox listBox = addListBoxWidget(cssProperty, title, setOptionsStyles, values, addStyleNames); final ToolbarButtonInfo buttonInfo = new ToolbarButtonInfo() { @Override public void unset(Element elem) {} @Override public void set(Element elem) { String selectedValue = getListBoxValue(listBox); elem.getStyle().setProperty(cssProperty, selectedValue); } @Override public boolean isSet(Element elem) { // since we don't do anything in "unSet", // always cause a change in the listbox to be applied, don't bother detecting if the given element // already has the correct style. return false; // String selectedValue = getListBoxValue(listBox); // boolean isSet = that.isCssPropertySet(cssProperty, new String[] { selectedValue }, testedElement); } @Override public boolean isOnRootElemOnly() { return false; } @Override public void updateButtonStatus(Element testedElement) { updateValueListBoxStatus(cssProperty, setOptionsStyles, listBox, testedElement); } private String getListBoxValue(final ListBox listBox) { return listBox.getValue(listBox.getSelectedIndex()); } }; // Deliberately not added to this.registrationsManager // because after onUnload+onLoad we currently won't be restoring these registrations properly listBox.addChangeHandler(new ChangeHandler() { @Override public void onChange(ChangeEvent event) { that.buttonPressed(buttonInfo); updateListBoxSelectItemStyle(setOptionsStyles, listBox); } }); updateListBoxSelectItemStyle(setOptionsStyles, listBox); this.addButtonInfo(buttonInfo); } private ListBox addListBoxWidget(final String cssProperty, String title, final boolean setOptionsStyles, final Iterable<String> values, String... addStyleNames) { final ListBox listBox = new ListBox(); listBox.addStyleName(CanvasResources.INSTANCE.main().canvasToolbarListBox()); for (String styleName : addStyleNames) { listBox.addStyleName(styleName); } for (String item : values) { listBox.addItem(item); if (setOptionsStyles) { OptionElement optionElement = ListBoxUtils.getOptionElement(listBox, listBox.getItemCount() - 1); optionElement.getStyle().setProperty(cssProperty, item); } } addTitledToolbarItem(title, listBox); return listBox; } private void addTitledToolbarItem(String title, Widget widget) { FlowPanel listBoxWrapper = new FlowPanel(); listBoxWrapper.addStyleName(CanvasResources.INSTANCE.main().canvasToolbarItemWrapper()); InlineLabel titleLabel = new InlineLabel(title); titleLabel.addStyleName(CanvasResources.INSTANCE.main().canvasToolbarItemTitle()); listBoxWrapper.add(titleLabel); listBoxWrapper.add(widget); this.rootPanel.add(listBoxWrapper); } /** * A wrapper for {@link #addCssStringValueButton(String, String[], String[], String)}, that create arrays with * a single value for unset and set value arrays. */ private void addCssStringValueButton(final String cssProperty, final String unsetValue, final String setValue, final String title, boolean onRootElemOnly) { this.addCssStringValueButton(cssProperty, new String[] { unsetValue }, new String[] { setValue }, title, onRootElemOnly); } /** * Creates a button that toggles a css property between two states. It will use the first value in each of the given * arrays (see below) to set the property, and will use the arrays themselves for testing the current value of the * property to decide if it is currently set or unset. * * @param cssProperty * Name of property to toggle, in camel case format ("fontStyle", not "font-style") * @param unsetValues * An array, with at least one element, of equivalent values for the unset state * @param setValues * An array, with at least one element, of equivalent values for the set state * @param title * Of the button */ private void addCssStringValueButton(final String cssProperty, final String[] unsetValues, final String[] setValues, final String title, final boolean onRootElemOnly) { assert (unsetValues.length >= 1); assert (setValues.length >= 1); final Button buttonWidget = createButtonWidget(cssProperty, setValues, title); ToolbarButtonInfo buttonInfo = new ToolbarButtonInfo() { @Override public boolean isSet(Element elem) { return isCssPropertySet(cssProperty, setValues, elem); } @Override public void set(Element elem) { elem.getStyle().setProperty(cssProperty, setValues[0]); } @Override public void unset(Element elem) { elem.getStyle().setProperty(cssProperty, unsetValues[0]); } @Override public boolean isOnRootElemOnly() { return onRootElemOnly; } @Override public void updateButtonStatus(Element testedElement) { // TODO chang eth button style if (this.isSet(testedElement)) { buttonWidget.addStyleName("gwt-ToggleButton-down"); buttonWidget.removeStyleName("gwt-ToggleButton-up"); } else { buttonWidget.removeStyleName("gwt-ToggleButton-down"); buttonWidget.addStyleName("gwt-ToggleButton-up"); } } }; this.addButton(buttonWidget, buttonInfo); } private Button createButtonWidget(final String cssProperty, final String[] setValues, final String title) { // Must be a button element, otherwise when it's clicked it will remove the current selection because the // browser // will see it as clicking on text. Button buttonWidget = new Button(); buttonWidget.getElement().getStyle().setProperty(cssProperty, setValues[0]); buttonWidget.getElement().setInnerText(title); buttonWidget.addStyleName(CanvasResources.INSTANCE.main().canvasToolbarButton()); return buttonWidget; } private void addButton(Button widget, final ToolbarButtonInfo buttonInfo) { final TextEditToolbarImpl that = this; this.rootPanel.add(widget); // Deliberately not added to this.registrationsManager // because after onUnload+onLoad we currently won't be restoring these registrations properly widget.addClickHandler(new ClickHandler() { @Override public void onClick(ClickEvent event) { that.buttonPressed(buttonInfo); } }); this.addButtonInfo(buttonInfo); } private void addButtonInfo(ToolbarButtonInfo buttonInfo) { this.buttonInfos.add(buttonInfo); } protected void updateButtonStates() { final Widget editedElement = this.getEditedWidget(); if (null == editedElement) { return; } this.saveSelectedRanges(); Element testElement = this.getTestElementFromSelection(); for (ToolbarButtonInfo buttonInfo : this.buttonInfos) { buttonInfo.updateButtonStatus(testElement); } } /** * Tries to find an element from within the selection range for testing whether a property is set or not in the range. */ private Element getTestElementFromSelection() { Element testElement = null; for (Range range : this.savedRanges) { HashMap<Node, Boolean> nodeContainmentMap = RangeUtils.getNodeContainmentMap(range); for (Node node : nodeContainmentMap.keySet()) { if (Node.TEXT_NODE != node.getNodeType()) { continue; } testElement = node.getParentElement(); if (testElement.getInnerText().isEmpty()) { continue; } return testElement; } } if (null != testElement) { return testElement; } if (this.isActiveElementTree()) { SelectionImpl selection = SelectionImpl.getWindowSelection(); Node node = selection.getAnchorNode(); if (null == node) { node = selection.getFocusNode(); } if (null != node) { return node.getParentElement(); } } return this._editedWidget.getElement(); } private void buttonPressed(final ToolbarButtonInfo buttonInfo) { final Widget editedElement = this.getEditedWidget(); if (null == editedElement) { return; } if (buttonInfo.isOnRootElemOnly()) { this.applyButtonOnRootElement(buttonInfo, editedElement.getElement()); } else { this.applyButtonOnSelectedRange(buttonInfo, editedElement.getElement()); } this._buttonAppliedEvent.dispatch(null); this._editedWidget.getElement().focus(); } private void saveSelectedRanges() { if (null == this._editedWidget) { return; } if (false == this.isActiveElementTree()) { return; } SelectionImpl selection = SelectionImpl.getWindowSelection(); if (0 >= selection.getRangeCount()) { return; } this.savedRanges.clear(); for (int i = 0; i < selection.getRangeCount(); i++) { Range range = selection.getRangeAt(i); this.savedRanges.add(range.cloneRange()); } } private void applyButtonOnSelectedRange(final ToolbarButtonInfo buttonInfo, final Element editedElement) { boolean isSetResult = this.isSetInSelection(buttonInfo); for (Range range : this.savedRanges) { Action<Element> action = null; if (isSetResult) { action = new Action<Element>() { @Override public void exec(Element arg) { buttonInfo.unset(arg); } }; } else { action = new Action<Element>() { @Override public void exec(Element arg) { buttonInfo.set(arg); } }; } RangeUtils.applyToNodesInRange(range, action); } // TODO this kills the range's validity... this.pushStylesInChildren(); ElementUtils.mergeSpans(editedElement); } private boolean isSetInSelection(final ToolbarButtonInfo buttonInfo) { Element testElement = this.getTestElementFromSelection(); return testElement == null ? false : buttonInfo.isSet(testElement); } private void applyButtonOnRootElement(final ToolbarButtonInfo buttonInfo, final Element editedElement) { if (buttonInfo.isSet(editedElement)) { buttonInfo.unset(editedElement); } else { buttonInfo.set(editedElement); } } private void pushStylesInChildren() { // We really should have run pushStylesDownToTextNodes on the edited // element itself rather than iterating and running the code on the // children. // But in TextEdit that would mean moving properties such as Width and // Height down into the children, which isn't what we want. for (Node node : ElementUtils.getChildNodes(this.getEditedWidget().getElement())) { if (Node.ELEMENT_NODE != node.getNodeType()) { continue; } Element childElem = Element.as(node); StyleUtils.pushStylesDownToTextNodes(childElem); } } private Boolean isCssPropertySet(final String cssProperty, final String[] setValues, Element element) { String currentValue = null; if (Objects.equal(cssProperty, "textDecoration")) { currentValue = StyleUtils.getInheritedTextDecoration(element); } else { currentValue = StyleUtils.getComputedStyle(element, null).getProperty(cssProperty); } if (null == currentValue) { return false; } for (String setValue : setValues) { if (Objects.equal(setValue, "")) { if ((currentValue.equals("inherit") || (currentValue.equals("")))) { // treat empty values as inherit, and only consider "" set if the css property is set or is inherit return true; } continue; } if (currentValue.contains(setValue)) { return true; } } return false; } private static void updateListBoxSelectItemStyle(final boolean setOptionsStyles, final ListBox listBox) { if (setOptionsStyles) { int selectedIndex = listBox.getSelectedIndex(); StyleUtils.copyStyle(listBox.getElement(), ListBoxUtils.getOptionElement(listBox, selectedIndex), true); } } private static String getCssPropertyValue(final String cssProperty, Element testedElement) { String value = StyleUtils.getComputedStyle(testedElement, null).getProperty(cssProperty); if (null == value) { return ""; } if (Objects.equal(cssProperty, "fontFamily")) { // css heuristic: pick out only the first part of the value value = value.split("[,]")[0]; } return value; } private static void updateValueListBoxStatus(final String cssProperty, final boolean setOptionsStyles, final ListBox listBox, Element testedElement) { String value = getCssPropertyValue(cssProperty, testedElement); boolean found = false; int selectedIndex = 0; for (int i = 0; i < listBox.getItemCount(); i++) { if (Objects.equal(listBox.getValue(i).toLowerCase(), value.toLowerCase())) { selectedIndex = i; found = true; break; } } if (false == found) { selectedIndex = listBox.getItemCount() - 1; listBox.addItem(value); } listBox.setSelectedIndex(selectedIndex); updateListBoxSelectItemStyle(setOptionsStyles, listBox); } }